Micron Document
🎖️GitЯра🎖️

Node / meshtastic / Meshtastic-Android / files / core / database / src / commonTest / kotlin / org / meshtastic / core / database / DatabaseManagerAssociationRecoveryTest.kt

Displaying Raw • Download

core/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseManagerAssociationRecoveryTest.kt d5848ad5e02bd9b5b1726bedfa51ceb0faaa0240 (d5848ad5) Text, 23.36 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.database

Tff7b72import T7ee787androidx.datastore.preferences.core.edit
Tff7b72import T7ee787androidx.datastore.preferences.core.stringPreferencesKey
Tff7b72import T7ee787kotlinx.coroutines.CancellationException
Tff7b72import T7ee787kotlinx.coroutines.CompletableDeferred
Tff7b72import T7ee787kotlinx.coroutines.Job
Tff7b72import T7ee787kotlinx.coroutines.cancelAndJoin
Tff7b72import T7ee787kotlinx.coroutines.flow.first
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.test.advanceTimeBy
Tff7b72import T7ee787kotlinx.coroutines.test.runTest
Tff7b72import T7ee787org.meshtastic.core.database.entity.MyNodeEntity
Tff7b72import T7ee787kotlin.test.AfterTest
Tff7b72import T7ee787kotlin.test.BeforeTest
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertFalse
Tff7b72import T7ee787kotlin.test.assertNotNull
Tff7b72import T7ee787kotlin.test.assertNull
Tff7b72import T7ee787kotlin.test.assertTrue

Tff7b72class T56d364DatabaseManagerAssociationRecoveryTest Tb4b4b4: Te6edf3DatabaseManagerTestFixtureTb4b4b4(Tb4b4b4) Tb4b4b4{

Tf0883e@BeforeTest Tff7b72fun Td2a8ffsetUpTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3setUpFixtureTb4b4b4(Tb4b4b4)

Tf0883e@AfterTest Tff7b72fun Td2a8fftearDownTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3tearDownFixtureTb4b4b4(Tb4b4b4)

Tf0883e@Test
Tff7b72fun Td2a8ffstaleAddressIdentityCannotClaimTheActiveDatabaseTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrATa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3active Tff7b72= Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3value

T8b949e// meshPrefs can publish addrB before switchActiveDatabase finishes while the previous node identity is
T8b949e// still
T8b949e// visible. The identity emission is bound to addrB and must not claim addrA's currently active database.
Te6edf3managerTb4b4b4.Te6edf3associateDeviceTb4b4b4(
Te6edf3address Tff7b72= Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4,
Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4,
Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4,
Te6edf3isSessionActive Tff7b72= Tb4b4b4{ Tff7b72true Tb4b4b4}Tb4b4b4,
Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3value Tff7b72=Tff7b72=Tff7b72= Te6edf3activeTb4b4b4)
Tff7b72val Te6edf3prefs Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffdevice_db_for:Tffd700${Te6edf3deviceKeyHexTb4b4b4(Ta5d6ff"Te6edf3deadbeefdeadbeefTa5d6ff"Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffnode_db_for:123Ta5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffaddr_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Tb4b4b4}

T8b949e// Association and recovery tests

T8b949e/**
* 1 + 2 combined: A writer holding the source DB's withDb lane causes the drain to time out, aborting the merge and
* restoring the source. After releasing the writer, a retry succeeds — the merge commits, dest becomes canonical,
* and the active DB switches.
*/
Tf0883e@Test
Tff7b72fun Td2a8ffdrainTimeoutRestoresSourceAndRetrySucceedsTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3dbATb4b4b4, Te6edf3dbBTb4b4b4) Tff7b72= Te6edf3setupTwoDatabasesTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3dbBTb4b4b4, Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4, Ta5d6ff"Ta5d6ffaddrB's DB should be active before associationTa5d6ff"Tb4b4b4)

T8b949e// Hold a withDb writer on addrB's DB so drainWriters can't complete.
Tff7b72val Te6edf3gate Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3writerJob Tff7b72= Te6edf3launch Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3withDb Tb4b4b4{
Te6edf3gateTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Tff7b72null
Tb4b4b4}
Tb4b4b4}

T8b949e// associateDevice should reach the merge branch, publish dest, then time out on the drain.
Tff7b72val Te6edf3associateJob Tff7b72= Te6edf3launch Tb4b4b4{ Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4) Tb4b4b4}

T8b949e// Advance virtual time past WRITER_DRAIN_TIMEOUT_MS (5_000ms) to trigger the drain timeout.
Te6edf3advanceTimeByTb4b4b4(T79c0ff5Te6edf3_501Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3associateJobTb4b4b4.Te6edf3isCompletedTb4b4b4, Ta5d6ff"Ta5d6ffassociateDevice should complete after drain timeoutTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3dbBTb4b4b4, Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4, Ta5d6ff"Ta5d6ffsource (addrB) should be restored after drain timeoutTa5d6ff"Tb4b4b4)

T8b949e// Release the writer gate so the source DB quiesces.
Te6edf3gateTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3writerJobTb4b4b4.Te6edf3joinTb4b4b4(Tb4b4b4)

T8b949e// Retry: with no writer in-flight, the drain succeeds and the merge commits.
Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(
Te6edf3dbATb4b4b4,
Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffdest (addrA's canonical DB) should be active after successful mergeTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

T8b949e/**
* 3: Cancelling [associateDevice] during the writer-drain wait propagates [CancellationException], restores the
* source DB, and does not leak the drain waiter — a subsequent retry succeeds.
*/
Tf0883e@Test
Tff7b72fun Td2a8ffcancellationDuringDrainRestoresSourceTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3dbATb4b4b4, Te6edf3dbBTb4b4b4) Tff7b72= Te6edf3setupTwoDatabasesTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3gate Tff7b72= Te6edf3CompletableDeferredTff7b72<Tffa657UnitTff7b72>Tb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3writerJob Tff7b72= Te6edf3launch Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3withDb Tb4b4b4{
Te6edf3gateTb4b4b4.Te6edf3awaitTb4b4b4(Tb4b4b4)
Tff7b72null
Tb4b4b4}
Tb4b4b4}

Tff7b72val Te6edf3associateJob Tff7b72= Te6edf3launch Tb4b4b4{ Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4) Tb4b4b4}

T8b949e// Cancel while suspended in drainWriters (before the 5s timeout fires).
Te6edf3associateJobTb4b4b4.Te6edf3cancelAndJoinTb4b4b4(Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3associateJobTb4b4b4.Te6edf3isCancelledTb4b4b4, Ta5d6ff"Ta5d6ffassociateDevice coroutine should be cancelledTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(
Te6edf3dbBTb4b4b4,
Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffsource should be restored after cancellation (NonCancellable publishActiveDb)Ta5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// Release the writer and retry — if the drain waiter leaked, the retry would also stall.
Te6edf3gateTb4b4b4.Te6edf3completeTb4b4b4(Tffa657UnitTb4b4b4)
Te6edf3writerJobTb4b4b4.Te6edf3joinTb4b4b4(Tb4b4b4)

Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(
Te6edf3dbATb4b4b4,
Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4,
Ta5d6ff"Ta5d6ffretry should succeed after cancellation cleanup — dest is canonicalTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

T8b949e/**
* 4: A successful association persists device, node, and address routing metadata in one atomic DataStore edit, all
* pointing to the same canonical DB.
*/
Tf0883e@Test
Tff7b72fun Td2a8ffatomicRoutingMetadataAllPointToCanonicalTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrATa5d6ff"Tb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)

T8b949e// No writer is held, so the drain succeeds immediately and the merge commits.
Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4)

Tff7b72val Te6edf3prefs Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3deviceClaim Tff7b72= Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffdevice_db_for:Tffd700${Te6edf3deviceKeyHexTb4b4b4(Ta5d6ff"Te6edf3deadbeefdeadbeefTa5d6ff"Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)Tff7b72]
Tff7b72val Te6edf3nodeClaim Tff7b72= Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffnode_db_for:123Ta5d6ff"Tb4b4b4)Tff7b72]
Tff7b72val Te6edf3addrClaim Tff7b72= Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffaddr_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]

Te6edf3assertNotNullTb4b4b4(Te6edf3deviceClaimTb4b4b4, Ta5d6ff"Ta5d6ffdevice-id claim should be persistedTa5d6ff"Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3nodeClaimTb4b4b4, Ta5d6ff"Ta5d6ffnode-num claim should be persistedTa5d6ff"Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3addrClaimTb4b4b4, Ta5d6ff"Ta5d6ffaddress alias should be persistedTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3deviceClaimTb4b4b4, Te6edf3nodeClaimTb4b4b4, Ta5d6ff"Ta5d6ffdevice and node claims point to the same DBTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3deviceClaimTb4b4b4, Te6edf3addrClaimTb4b4b4, Ta5d6ff"Ta5d6ffaddress alias points to the same canonical DBTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffpendingRouteIsPersistedBeforeMergeAndRemovedWithFinalRoutingTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3setupTwoDatabasesTb4b4b4(Tb4b4b4)
Tff7b72var Te6edf3pendingObserved Tff7b72= Tff7b72false
Te6edf3managerTb4b4b4.Te6edf3beforeMerge Tff7b72= Tb4b4b4{ Te6edf3_Tb4b4b4, Te6edf3_Tb4b4b4, Te6edf3sourceName Tff7b72-Tff7b72>
Tff7b72val Te6edf3prefs Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3sourceNameTb4b4b4, Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3buildDbNameTb4b4b4(Ta5d6ff"Ta5d6ffaddrATa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3pendingObserved Tff7b72= Tff7b72true
Tb4b4b4}

Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3pendingObservedTb4b4b4, Ta5d6ff"Ta5d6ffpending route must be durable before merge startsTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3prefs Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffpreCommitMergeFailureClearsPendingRouteAndKeepsSourceTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3_Tb4b4b4, Te6edf3sourceTb4b4b4) Tff7b72= Te6edf3setupTwoDatabasesTb4b4b4(Tb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3failMergeWith Tff7b72= Te6edf3RuntimeExceptionTb4b4b4(Ta5d6ff"Ta5d6ffpre-commit failureTa5d6ff"Tb4b4b4)

Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3sourceTb4b4b4, Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4)
Tff7b72val Te6edf3prefs Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3managerTb4b4b4.Te6edf3debugWriterGateArmedTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffstalePendingRouteWithoutMarkerIsDiscardedTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3fallback Tff7b72= Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3value
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Tff7b72nullTb4b4b4)
Te6edf3armableDsTb4b4b4.Te6edf3edit Tb4b4b4{
Tffa657itTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72] Tff7b72= Te6edf3buildDbNameTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)
Tffa657itTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72] Tff7b72= Te6edf3buildDbNameTb4b4b4(Ta5d6ff"Ta5d6ffaddrATa5d6ff"Tb4b4b4)
Tb4b4b4}

Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3fallbackTb4b4b4, Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4, Ta5d6ff"Ta5d6ffmarker-free pending route must use the normal fallbackTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3prefs Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffaddr_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffcommittedPendingRouteForCurrentAddressSwitchesToRepairedDestinationTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3destinationTb4b4b4, Te6edf3sourceTb4b4b4) Tff7b72= Te6edf3setupTwoDatabasesTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3sourceName Tff7b72= Te6edf3buildDbNameTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3destinationName Tff7b72= Te6edf3buildDbNameTb4b4b4(Ta5d6ff"Ta5d6ffaddrATa5d6ff"Tb4b4b4)
Te6edf3armableDsTb4b4b4.Te6edf3edit Tb4b4b4{
Tffa657itTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72] Tff7b72= Te6edf3sourceName
Tffa657itTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72] Tff7b72= Te6edf3destinationName
Tb4b4b4}
Te6edf3managerTb4b4b4.Te6edf3markerVerification Tff7b72= Tb4b4b4{ Te6edf3databaseTb4b4b4, Te6edf3pendingSource Tff7b72-Tff7b72>
Te6edf3database Tff7b72=Tff7b72=Tff7b72= Te6edf3destination Tff7b72&Tff7b72& Te6edf3pendingSource Tff7b72=Tff7b72= Te6edf3sourceName
Tb4b4b4}

Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3value Tff7b72=Tff7b72=Tff7b72= Te6edf3destinationTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3value Tff7b72!Tff7b72=Tff7b72= Te6edf3sourceTb4b4b4)
Tff7b72val Te6edf3prefs Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3destinationNameTb4b4b4, Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffaddr_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffcommittedPendingRouteRepairsStaleExistingAliasBeforePublishingTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3destinationTb4b4b4, Te6edf3_Tb4b4b4) Tff7b72= Te6edf3setupTwoDatabasesTb4b4b4(Tb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3afterMergeCommitted Tff7b72= Tb4b4b4{
Te6edf3armableDsTb4b4b4.Te6edf3armFailBeforeCommitTb4b4b4(Te6edf3RuntimeExceptionTb4b4b4(Ta5d6ff"Ta5d6ffsimulated final-routing failureTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4)
Te6edf3armableDsTb4b4b4.Te6edf3edit Tb4b4b4{ Tffa657itTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffaddr_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72] Tff7b72= Te6edf3buildDbNameTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4) Tb4b4b4}

Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Tff7b72nullTb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3destinationTb4b4b4, Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4, Ta5d6ff"Ta5d6ffmerge marker must override the stale source aliasTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3repaired Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3buildDbNameTb4b4b4(Ta5d6ff"Ta5d6ffaddrATa5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3repairedTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffaddr_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3repairedTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3repairedTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffrecoveredSourceAfterDevicePublicationIsProtectedUntilShutdownTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffalready-publishedTa5d6ff"Tb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Tff7b72nullTb4b4b4)
Tff7b72val Te6edf3sourceName Tff7b72= Te6edf3buildDbNameTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3destinationName Tff7b72= Te6edf3buildDbNameTb4b4b4(Ta5d6ff"Ta5d6ffaddrATa5d6ff"Tb4b4b4)
Te6edf3armableDsTb4b4b4.Te6edf3edit Tb4b4b4{
Tffa657itTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72] Tff7b72= Te6edf3sourceName
Tffa657itTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72] Tff7b72= Te6edf3destinationName
Tb4b4b4}
Te6edf3managerTb4b4b4.Te6edf3markerVerification Tff7b72= Tb4b4b4{ Te6edf3_Tb4b4b4, Te6edf3_ Tff7b72-Tff7b72> Tff7b72true Tb4b4b4}

Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3managerTb4b4b4.Te6edf3deletedDatabaseNamesTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffsame-process recovery must defer source deletionTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3sourceName Tff7b72in Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)Tff7b72[Te6edf3retiredDbNamesKeyTff7b72]Tb4b4b4.Te6edf3orEmptyTb4b4b4(Tb4b4b4)Tb4b4b4)

Te6edf3managerTb4b4b4.Te6edf3closeTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3listOfTb4b4b4(Te6edf3sourceNameTb4b4b4)Tb4b4b4, Te6edf3managerTb4b4b4.Te6edf3deletedDatabaseNamesTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffcancellationAfterRecoveredRouteCommitStillProtectsPublishedSourceTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffalready-publishedTa5d6ff"Tb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Tff7b72nullTb4b4b4)
Tff7b72val Te6edf3sourceName Tff7b72= Te6edf3buildDbNameTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3destinationName Tff7b72= Te6edf3buildDbNameTb4b4b4(Ta5d6ff"Ta5d6ffaddrATa5d6ff"Tb4b4b4)
Te6edf3armableDsTb4b4b4.Te6edf3edit Tb4b4b4{
Tffa657itTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72] Tff7b72= Te6edf3sourceName
Tffa657itTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72] Tff7b72= Te6edf3destinationName
Tb4b4b4}
Te6edf3managerTb4b4b4.Te6edf3markerVerification Tff7b72= Tb4b4b4{ Te6edf3_Tb4b4b4, Te6edf3_ Tff7b72-Tff7b72> Tff7b72true Tb4b4b4}

Tff7b72val Te6edf3recovery Tff7b72= Te6edf3launch Tb4b4b4{
Te6edf3armableDsTb4b4b4.Te6edf3armCancelAfterCommitTb4b4b4(Te6edf3coroutineContextTff7b72[Te6edf3JobTff7b72]Tff7b72!!Tb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)
Tb4b4b4}
Te6edf3recoveryTb4b4b4.Te6edf3joinTb4b4b4(Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3recoveryTb4b4b4.Te6edf3isCancelledTb4b4b4, Ta5d6ff"Ta5d6ffrecovery caller should observe cancellation after the durable commitTa5d6ff"Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3managerTb4b4b4.Te6edf3debugIsLogicallyRetiredTb4b4b4(Te6edf3sourceNameTb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffthe published source must remain eviction-protectedTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3managerTb4b4b4.Te6edf3deletedDatabaseNamesTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffsame-process recovery must not physically reclaim sourceTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tff7b72val Te6edf3prefs Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3destinationNameTb4b4b4, Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffaddr_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3sourceName Tff7b72in Te6edf3prefsTff7b72[Te6edf3retiredDbNamesKeyTff7b72]Tb4b4b4.Te6edf3orEmptyTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ffmarkerVerificationFailureDoesNotPublishFallbackTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3original Tff7b72= Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3value
Te6edf3armableDsTb4b4b4.Te6edf3edit Tb4b4b4{
Tffa657itTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72] Tff7b72= Te6edf3buildDbNameTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)
Tffa657itTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72] Tff7b72= Te6edf3buildDbNameTb4b4b4(Ta5d6ff"Ta5d6ffaddrATa5d6ff"Tb4b4b4)
Tb4b4b4}
Te6edf3managerTb4b4b4.Te6edf3markerVerification Tff7b72= Tb4b4b4{ Te6edf3_Tb4b4b4, Te6edf3_ Tff7b72-Tff7b72> Tff7b72throw Te6edf3TestErrorTb4b4b4(Ta5d6ff"Ta5d6ffmarker read failedTa5d6ff"Tb4b4b4) Tb4b4b4}

Tff7b72var Te6edf3caughtTb4b4b4: Te6edf3TestError? Tff7b72= Tff7b72null
Tff7b72try Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3switchActiveDatabaseTb4b4b4(Ta5d6ff"Ta5d6ffaddrBTa5d6ff"Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3errorTb4b4b4: Te6edf3TestErrorTb4b4b4) Tb4b4b4{
Te6edf3caught Tff7b72= Te6edf3error
Tb4b4b4}

Te6edf3assertNotNullTb4b4b4(Te6edf3caughtTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3originalTb4b4b4, Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4, Ta5d6ff"Ta5d6fffallback source must never publish without marker proofTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3managerTb4b4b4.Te6edf3currentAddressTb4b4b4.Te6edf3valueTb4b4b4)
Tb4b4b4}

T8b949e/**
* Commit-then-cancel: the routing DataStore edit commits durably, then the wrapper throws CancellationException.
* Destination must remain active, all routing metadata must agree, and cancellation must propagate.
*/
Tf0883e@Test
Tff7b72fun Td2a8ffcancellationAfterMergeCommitKeepsDestinationTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3dbATb4b4b4, Te6edf3dbBTb4b4b4) Tff7b72= Te6edf3setupTwoDatabasesTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3dbBTb4b4b4, Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4, Ta5d6ff"Ta5d6ffaddrB should be active before associationTa5d6ff"Tb4b4b4)

Te6edf3managerTb4b4b4.Te6edf3afterMergeCommitted Tff7b72= Tb4b4b4{ Te6edf3armableDsTb4b4b4.Te6edf3armFailAfterCommitTb4b4b4(Te6edf3CancellationExceptionTb4b4b4(Ta5d6ff"Ta5d6ffpost-merge cancelTa5d6ff"Tb4b4b4)Tb4b4b4) Tb4b4b4}

Tff7b72var Te6edf3caughtTb4b4b4: Te6edf3CancellationException? Tff7b72= Tff7b72null
Tff7b72try Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3eTb4b4b4: Te6edf3CancellationExceptionTb4b4b4) Tb4b4b4{
Te6edf3caught Tff7b72= Te6edf3e
Tb4b4b4}
Te6edf3assertNotNullTb4b4b4(Te6edf3caughtTb4b4b4, Ta5d6ff"Ta5d6ffCancellationException should propagate after merge commitTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3dbATb4b4b4, Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4, Ta5d6ff"Ta5d6ffdestination must remain active — source is NOT restoredTa5d6ff"Tb4b4b4)

T8b949e// Routing metadata was durably committed before the wrapper threw. All claims must agree.
Tff7b72val Te6edf3prefs Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3deviceClaim Tff7b72= Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffdevice_db_for:Tffd700${Te6edf3deviceKeyHexTb4b4b4(Ta5d6ff"Te6edf3deadbeefdeadbeefTa5d6ff"Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)Tff7b72]
Tff7b72val Te6edf3nodeClaim Tff7b72= Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffnode_db_for:123Ta5d6ff"Tb4b4b4)Tff7b72]
Tff7b72val Te6edf3addrClaim Tff7b72= Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffaddr_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]
Te6edf3assertNotNullTb4b4b4(Te6edf3deviceClaimTb4b4b4, Ta5d6ff"Ta5d6ffdevice claim committedTa5d6ff"Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3nodeClaimTb4b4b4, Ta5d6ff"Ta5d6ffnode claim committedTa5d6ff"Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3addrClaimTb4b4b4, Ta5d6ff"Ta5d6ffaddress alias committedTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3deviceClaimTb4b4b4, Te6edf3nodeClaimTb4b4b4, Ta5d6ff"Ta5d6ffdevice and node claims agreeTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3deviceClaimTb4b4b4, Te6edf3addrClaimTb4b4b4, Ta5d6ff"Ta5d6ffaddress alias agrees with claimsTa5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e/**
* DataStore failure after merge: final routing does not commit. Destination stays active, the pending route remains
* as crash-recovery intent, and an ordinary in-process retry can also finalize all routing metadata.
*/
Tf0883e@Test
Tff7b72fun Td2a8ffdataStoreFailureAfterMergeKeepsDestinationAndRepairsOnRetryTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3dbATb4b4b4, Te6edf3dbBTb4b4b4) Tff7b72= Te6edf3setupTwoDatabasesTb4b4b4(Tb4b4b4)

Te6edf3managerTb4b4b4.Te6edf3afterMergeCommitted Tff7b72= Tb4b4b4{
Te6edf3armableDsTb4b4b4.Te6edf3armFailBeforeCommitTb4b4b4(Te6edf3RuntimeExceptionTb4b4b4(Ta5d6ff"Ta5d6ffsimulated DataStore failureTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3dbATb4b4b4, Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4, Ta5d6ff"Ta5d6ffdest must remain active — source is NOT restoredTa5d6ff"Tb4b4b4)

T8b949e// Before retry: routing metadata was NOT persisted (FailBeforeCommit prevented the edit).
Tff7b72val Te6edf3prefsBefore Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertNullTb4b4b4(
Te6edf3prefsBeforeTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffaddr_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4,
Ta5d6ff"Ta5d6ffaddress alias must be absent before repair — routing edit failedTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(
Te6edf3prefsBeforeTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4,
Ta5d6ff"Ta5d6ffpending source must survive a post-commit routing failureTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(
Te6edf3prefsBeforeTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4,
Ta5d6ff"Ta5d6ffpending destination must survive a post-commit routing failureTa5d6ff"Tb4b4b4,
Tb4b4b4)

T8b949e// Retry repairs routing metadata via the already-unified branch.
Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4)

Tff7b72val Te6edf3prefsAfter Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Tff7b72val Te6edf3deviceClaim Tff7b72= Te6edf3prefsAfterTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffdevice_db_for:Tffd700${Te6edf3deviceKeyHexTb4b4b4(Ta5d6ff"Te6edf3deadbeefdeadbeefTa5d6ff"Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)Tff7b72]
Tff7b72val Te6edf3nodeClaim Tff7b72= Te6edf3prefsAfterTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffnode_db_for:123Ta5d6ff"Tb4b4b4)Tff7b72]
Tff7b72val Te6edf3addrClaim Tff7b72= Te6edf3prefsAfterTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffaddr_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]
Te6edf3assertNotNullTb4b4b4(Te6edf3deviceClaimTb4b4b4, Ta5d6ff"Ta5d6ffdevice claim repairedTa5d6ff"Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3nodeClaimTb4b4b4, Ta5d6ff"Ta5d6ffnode claim repairedTa5d6ff"Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3addrClaimTb4b4b4, Ta5d6ff"Ta5d6ffaddress alias repairedTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3deviceClaimTb4b4b4, Te6edf3nodeClaimTb4b4b4, Ta5d6ff"Ta5d6ffdevice and node claims agree after repairTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3deviceClaimTb4b4b4, Te6edf3addrClaimTb4b4b4, Ta5d6ff"Ta5d6ffaddress alias agrees after repairTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsAfterTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsAfterTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Tb4b4b4}

T8b949e/**
* After a post-merge routing failure, new writes land in destination (not source), proving source is never
* reactivated past the merge-commit boundary.
*/
Tf0883e@Test
Tff7b72fun Td2a8ffpostMergeWriteLandsInDestinationTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3dbATb4b4b4, Te6edf3dbBTb4b4b4) Tff7b72= Te6edf3setupTwoDatabasesTb4b4b4(Tb4b4b4)

Te6edf3managerTb4b4b4.Te6edf3afterMergeCommitted Tff7b72= Tb4b4b4{
Te6edf3armableDsTb4b4b4.Te6edf3armFailBeforeCommitTb4b4b4(Te6edf3RuntimeExceptionTb4b4b4(Ta5d6ff"Ta5d6ffsimulated DataStore failureTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4)

Tff7b72val Te6edf3destDb Tff7b72= Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3value
Te6edf3assertEqualsTb4b4b4(Te6edf3dbATb4b4b4, Te6edf3destDbTb4b4b4, Ta5d6ff"Ta5d6ffdestination is dbA (the canonical DB)Ta5d6ff"Tb4b4b4)

Te6edf3managerTb4b4b4.Te6edf3withDb Tb4b4b4{
Tffa657itTb4b4b4.Te6edf3nodeInfoDaoTb4b4b4(Tb4b4b4)
Tb4b4b4.Te6edf3setMyNodeInfoTb4b4b4(
Te6edf3MyNodeEntityTb4b4b4(
Te6edf3myNodeNum Tff7b72= T79c0ff9T79c0ff9T79c0ff9Tb4b4b4,
Te6edf3model Tff7b72= Tff7b72nullTb4b4b4,
Te6edf3firmwareVersion Tff7b72= Tff7b72nullTb4b4b4,
Te6edf3couldUpdate Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3shouldUpdate Tff7b72= Tff7b72falseTb4b4b4,
Te6edf3currentPacketId Tff7b72= T79c0ff0LTb4b4b4,
Te6edf3messageTimeoutMsec Tff7b72= T79c0ff0Tb4b4b4,
Te6edf3minAppVersion Tff7b72= T79c0ff0Tb4b4b4,
Te6edf3maxChannels Tff7b72= T79c0ff0Tb4b4b4,
Te6edf3hasWifi Tff7b72= Tff7b72falseTb4b4b4,
Tb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

T8b949e// The write exists in destination.
Tff7b72val Te6edf3destMyNode Tff7b72= Te6edf3destDbTb4b4b4.Te6edf3nodeInfoDaoTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3getMyNodeInfoTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3destMyNodeTb4b4b4, Ta5d6ff"Ta5d6ffpost-merge write landed in destinationTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff9T79c0ff9T79c0ff9Tb4b4b4, Te6edf3destMyNodeTb4b4b4.Te6edf3myNodeNumTb4b4b4, Ta5d6ff"Ta5d6ffwrite is durable in destinationTa5d6ff"Tb4b4b4)

T8b949e// The write does NOT exist in source.
Tff7b72val Te6edf3sourceMyNode Tff7b72= Te6edf3dbBTb4b4b4.Te6edf3nodeInfoDaoTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3getMyNodeInfoTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3sourceMyNodeTb4b4b4, Ta5d6ff"Ta5d6ffsource must not have the post-merge writeTa5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e/** A fatal failure before the merge commits still clears intent, releases the gate, and restores source. */
Tf0883e@Test
Tff7b72fun Td2a8fferrorBeforeMergeCommitReleasesGateAndRestoresSourceTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3_Tb4b4b4, Te6edf3sourceTb4b4b4) Tff7b72= Te6edf3setupTwoDatabasesTb4b4b4(Tb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3failMergeWith Tff7b72= Te6edf3TestErrorTb4b4b4(Ta5d6ff"Ta5d6fffatal pre-commit failureTa5d6ff"Tb4b4b4)

Tff7b72var Te6edf3caughtTb4b4b4: Te6edf3TestError? Tff7b72= Tff7b72null
Tff7b72try Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3errorTb4b4b4: Te6edf3TestErrorTb4b4b4) Tb4b4b4{
Te6edf3caught Tff7b72= Te6edf3error
Tb4b4b4}

Te6edf3assertNotNullTb4b4b4(Te6edf3caughtTb4b4b4, Ta5d6ff"Ta5d6fffatal failure must propagateTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3sourceTb4b4b4, Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4, Ta5d6ff"Ta5d6ffsource remains canonical before the commit boundaryTa5d6ff"Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3managerTb4b4b4.Te6edf3debugWriterGateArmedTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6fffatal failure must release writer admissionTa5d6ff"Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3writersTb4b4b4, Te6edf3waitersTb4b4b4) Tff7b72= Te6edf3managerTb4b4b4.Te6edf3debugWriterCountsTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3writersTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3waitersTb4b4b4)
Tff7b72val Te6edf3prefs Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Tb4b4b4}

T8b949e/** A fatal final-routing failure after commit propagates but keeps destination and its repair intent canonical. */
Tf0883e@Test
Tff7b72fun Td2a8fferrorAfterMergeCommitReleasesGateAndKeepsDestinationTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3runTestTb4b4b4(Te6edf3testDispatcherTb4b4b4) Tb4b4b4{
Tff7b72val Tb4b4b4(Te6edf3destinationTb4b4b4, Te6edf3_Tb4b4b4) Tff7b72= Te6edf3setupTwoDatabasesTb4b4b4(Tb4b4b4)
Te6edf3managerTb4b4b4.Te6edf3afterMergeCommitted Tff7b72= Tb4b4b4{ Te6edf3armableDsTb4b4b4.Te6edf3armFailBeforeCommitTb4b4b4(Te6edf3TestErrorTb4b4b4(Ta5d6ff"Ta5d6fffatal routing failureTa5d6ff"Tb4b4b4)Tb4b4b4) Tb4b4b4}

Tff7b72var Te6edf3caughtTb4b4b4: Te6edf3TestError? Tff7b72= Tff7b72null
Tff7b72try Tb4b4b4{
Te6edf3managerTb4b4b4.Te6edf3associateCurrentDeviceTb4b4b4(Te6edf3nodeNum Tff7b72= T79c0ff1T79c0ff2T79c0ff3Tb4b4b4, Te6edf3deviceId Tff7b72= Ta5d6ff"Ta5d6ffdeadbeefdeadbeefTa5d6ff"Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3errorTb4b4b4: Te6edf3TestErrorTb4b4b4) Tb4b4b4{
Te6edf3caught Tff7b72= Te6edf3error
Tb4b4b4}

Te6edf3assertNotNullTb4b4b4(Te6edf3caughtTb4b4b4, Ta5d6ff"Ta5d6fffatal failure must propagateTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3destinationTb4b4b4, Te6edf3managerTb4b4b4.Te6edf3currentDbTb4b4b4.Te6edf3valueTb4b4b4, Ta5d6ff"Ta5d6ffdestination remains canonical after merge commitTa5d6ff"Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3managerTb4b4b4.Te6edf3debugWriterGateArmedTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6fffatal failure must release writer admissionTa5d6ff"Tb4b4b4)
Tff7b72val Tb4b4b4(Te6edf3writersTb4b4b4, Te6edf3waitersTb4b4b4) Tff7b72= Te6edf3managerTb4b4b4.Te6edf3debugWriterCountsTb4b4b4(Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3writersTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Tb4b4b4, Te6edf3waitersTb4b4b4)
Tff7b72val Te6edf3prefs Tff7b72= Te6edf3armableDsTb4b4b4.Te6edf3dataTb4b4b4.Te6edf3firstTb4b4b4(Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_source_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3prefsTff7b72[Te6edf3stringPreferencesKeyTb4b4b4(Ta5d6ff"Ta5d6ffpending_destination_db_for:ADDRBTa5d6ff"Tb4b4b4)Tff7b72]Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.14s